Skip to content

feat: schema cache to prevent thundering herd - #1258

Merged
levkk merged 6 commits into
mainfrom
levkk-db-global-schema-cache
Jul 27, 2026
Merged

feat: schema cache to prevent thundering herd#1258
levkk merged 6 commits into
mainfrom
levkk-db-global-schema-cache

Conversation

@levkk

@levkk levkk commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Sharded deployments with dozens / hundreds of users will stampede the servers to load the schema. With large schemas, e.g., thousands of tables, and multiple replicas, this is pretty slow and will cause issues.

The cache is using the database name and shard number, which uniquely identifies a host in pgdog.toml.

@levkk
levkk requested review from meskill and sgrif July 23, 2026 21:54
@blacksmith-sh

This comment has been minimized.

@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Comment on lines +47 to +49
let schema = shard.fetch_schema().await?;

*guard = schema.clone();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in theory here could be a tricky data race with SchemaCache::global().clear();:

on replace_databases the following happens: clear -> new_databases.launch -> old_databases.shutdown

if for some reason we trigger replace_databases while schemas are still loading for old (like with retries due to errors) we may try to write into the cache that is basically associated with the new databases already.

The shutdown token would've be helpful, but this is called after the clear and launch, leaving this race window.

There is another race that is not important I think: lock -> fetch_schema -> reload_databases -> set to old guard. But it's not a big issue since the guard will not point to the cache map entry anymore and that'd be a separate mutex.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I know what to do here. I'll add a global "config is reloading" signal that can cancel all of these long-running loops. I've been meaning to add something like this; making everything more deterministic.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might also have to be versioned too. Each config reload increments the config version so each Cluster knows which "epoch" it belongs to.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*race condition, not a data race.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this one is kinda tough. We may need to refactor the config system from an ArcSwap to a watch, or something. to ensure this works across the whole app.

Right now, the race condition is real, but it may not have a real impact. i.e., if they race, they happen around the same time, so the schema is probably identical anyway. This was built specifically to busted when the user wants it to, e.g., they create a new table, then hit RELOAD in admin. Either way, the table already exists, and the schema loaded in the cache is fresh.

@levkk levkk Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured it out. We should attach all config-scoped "globals" to the Cluster. Duh. It's already versioned in a way that each config version gets its own Cluster, so as long as the cache is scoped to the lifetime of that Cluster, we don't have a race between config versions.

We should ban globals lol. Everything from now on should be Cluster-scoped.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 2fe3347

sgrif
sgrif previously requested changes Jul 24, 2026
Comment thread pgdog/src/backend/schema/cache/mod.rs Outdated
Comment thread pgdog/src/backend/schema/cache/mod.rs Outdated
Comment thread pgdog/src/backend/schema/cache/mod.rs
Comment thread pgdog/src/backend/schema/cache/mod.rs
Comment thread pgdog/src/backend/schema/cache/mod.rs
Comment thread pgdog/src/backend/schema/cache/mod.rs Outdated
levkk and others added 2 commits July 24, 2026 10:58
Comment thread pgdog/src/backend/pool/cluster.rs
@levkk
levkk requested a review from meskill July 24, 2026 19:35
impl<'a> ClusterConfig<'a> {
/// Dependencies for creating a Cluster.
///
/// TODO(lev): This is getting unruly. We may need a struct for a struct :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@levkk levkk Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. ClusterConfig was meant to be a "manual bon" for Cluster::new. So now we need a ClusterConfigConfig :)

@levkk
levkk dismissed sgrif’s stale review July 27, 2026 15:04

Going to roll forward with this as-is. If there are any issues, I'll refactor.

@levkk
levkk merged commit 6eb1d18 into main Jul 27, 2026
45 checks passed
@levkk
levkk deleted the levkk-db-global-schema-cache branch July 27, 2026 15:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants